package rayCaster;

import java.util.ArrayList;
import java.util.Random;
import static rayCaster.ApplicationInterface.*;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import primitive3D.Primitive3D;
import java.awt.Color;
import static java.lang.Math.*;

public class RayCaster {
	
	private int k = 0;
	
	public Pixel[] rayCast(Camera camera, Scene scene, int width, int height,
		COLOR_OPTIONS colorMode, COLOR_VAR_OPTIONS colorVariationMode) throws Exception {

		// Inicializo el modo de color de las primitivas de la escena
		ArrayList<Primitive3D> allFigures = scene.getFigures();
		if( colorMode == COLOR_OPTIONS.RANDOM ) {
			Random r = new Random();
			int color;
			for( Primitive3D figure : allFigures ) {
				color = colors[r.nextInt(colors.length)];
				figure.setColor(color);
			}
		}
		else {	// COLOR_OPTIONS.ORDERED
			for( Primitive3D figure : allFigures ) {
				// Todavia no a sido asignado un color a la figura, puedo
				// elegir el color que yo decida, no necesariamente el negro
				figure.setColor(NOT_ASIGNED_COLOR);
			}
		}
		
		// Preproceso para detectar la distancia maxima y minima de interseccion
		// del rayo con un objeto. Modo de variacion del color.
		for( int i=0; i< width; i++ ) {
			for( int j=0; j< height; j++ ) {
				Ray ray = constructRayThroughPixel( camera, i, j );
				Hit hit = findIntersection( ray, scene, colorVariationMode );
				preprocessDistance(hit);
			}
		}		
		
		// Comienzo a ejecutar el RayCasting
		ImagePlate canvas = camera.getCanvas();
		for( int i=0; i< width; i++ ) {
			for( int j=0; j< height; j++ ) {
				Ray ray = constructRayThroughPixel( camera, i, j );
				Hit hit = findIntersection( ray, scene, colorVariationMode );
				canvas.setPixel(i, j, getColor(hit));				
			}
		}		
		return canvas.getPixelsArray();
	}

	private void preprocessDistance(Hit hit) {
		Primitive3D figure = hit.getFigure();
		if( figure != null ) {
			double distance = hit.getDistance();
			double minDistance = figure.getMinDistanceFromCamera();
			double maxDistance = figure.getMaxDistanceFromCamera();
			if( distance < minDistance ) {
				figure.setMinDistanceFromCamera(distance);
			}
			else if( distance > maxDistance ) {
				figure.setMaxDistanceFromCamera(distance);
			}
		}
	}

	/*
	 * Defines what color the Pixel is to be painted, accordingly to the 
	 * distance from the camera.
	 */
	private Pixel getColor(Hit hit) {
		Primitive3D figure = hit.getFigure();
		int color = hit.getColor();
		// Se trata el modo de color ORDERED
		if( figure != null && color == NOT_ASIGNED_COLOR ) {
			color = colors[ (k++)%colors.length ];
			figure.setColor(color);
		}
		// Se trata el modo de variacion del color
		if( figure != null ) {
			double distance = hit.getDistance();
			double minDistance = figure.getMinDistanceFromCamera();
			double maxDistance = figure.getMaxDistanceFromCamera();
			double bChannel;
			COLOR_VAR_OPTIONS colorVariationMode = hit.getColorVariationMode();
			// Tratar el modo lineal
			if( colorVariationMode == COLOR_VAR_OPTIONS.LINEAR ) {
				bChannel = 1 - SHADOW_STRENGTH*(distance - minDistance)/(maxDistance - minDistance);
			}
			// Tratar el modo logaritmico
			else {	// COLOR_VAR_OPTIONS.LOG
				bChannel = 1 - SHADOW_STRENGTH*log10(distance/minDistance)/log10(maxDistance/minDistance);
			}
			// Modificar valor de brillo segun uno de los modos anteriores
			Color rgb = new Color(color);
			float[] hsb = Color.RGBtoHSB(rgb.getRed(), rgb.getGreen(), rgb.getBlue(), null);
			// Los valores de bChannel deben estar entre 0 y 1 (0 = Oscuro, 1 = Claro)
			color = Color.HSBtoRGB(hsb[0], hsb[1], (float)bChannel);
		}

		return new Pixel(color);
	}

	// Clase interna a RayCaster
	class Hit {
		int color;
		double distance;
		Primitive3D figure;
		COLOR_VAR_OPTIONS colorVariationMode;
		
		Hit(int color, double distance, Primitive3D figure, COLOR_VAR_OPTIONS colorVariationMode) {
			this.color = color;
			this.distance = distance;
			this.figure = figure;
			this.colorVariationMode = colorVariationMode;
		}

		int getColor() {
			return color;
		}
		
		double getDistance() {
			return distance;
		}
		
		Primitive3D getFigure() {
			return figure;
		}
		
		COLOR_VAR_OPTIONS getColorVariationMode() {
			return colorVariationMode;
		}
		
		public String toString() {
			return "Color: " +color+ "\tDistancia: " +distance+ "\tFigura: " +figure;
		}
	}
	
	/*
	 * Calculates the intersection between a ray and all the figures in
	 * the scene 
	 */	
	private Hit findIntersection(Ray ray, Scene scene, COLOR_VAR_OPTIONS colorVariationMode) throws Exception {
		// El fondo de la escena es blanco
		int cant, color = BACKGROUND_COLOR;
		double distance, minDistance = Double.POSITIVE_INFINITY;
		ArrayList<Primitive3D> allFigures = scene.getFigures();
		Primitive3D visibleFigure = null;
		Point3d[] pts = new Point3d[2];
		pts[0] = new Point3d();
		pts[1] = new Point3d();
		for( Primitive3D figure : allFigures ) {
			if( (cant = figure.intersects(ray, pts)) > 0 ) {
				for( int i=0; i < cant; i++ ) {
					Point3d origin = ray.getOrigin();
					distance = origin.distance(pts[i]);
					if( distance < minDistance ) {
						minDistance = distance;
						color = figure.getColor();
						visibleFigure = figure;
					}
				}
			}
		}
		return new Hit(color, minDistance, visibleFigure, colorVariationMode);
	}

	/*
	 * Build a ray that passes through the camera's image plate at the
	 * coordinates defined by x and y
	 */
	private Ray constructRayThroughPixel(Camera camera, int x, int y) {		
		Vector3d parallelVersorToCanvas = new Vector3d();
		Vector3d diffPosition = new Vector3d();
		Point3d upperLeftPoint = new Point3d();
		
		//Calculate the step size from the formula stated above
		ImagePlate canvas = camera.getCanvas();
		int width = canvas.getWidth();
		int height = canvas.getHeight();
		double fob = camera.getVisualAngle()/2;
		double stepSize =  2 *  ( camera.getCanvasDistance() / width ) * tan(fob);
		stepSize = abs(stepSize);		
		
		//Finds out the parallel versor to the canvas plane
		Vector3d cameraDirectionVersor = camera.getCameraDirection();		
		Vector3d verticalReferenceVersor = camera.getCameraVerticalReference();		
		parallelVersorToCanvas.cross(cameraDirectionVersor, verticalReferenceVersor);
		
		//Calculate the upper left corner of the plate
		Vector3d diffHorizontal = new Vector3d(parallelVersorToCanvas);
		Vector3d diffVertical = new Vector3d(verticalReferenceVersor);		
		diffVertical.scale( getScaleFactor(height, stepSize) );
		diffHorizontal.scale( getScaleFactor(width, stepSize) );
		diffPosition.sub( diffVertical, diffHorizontal );		
		upperLeftPoint.add(camera.getCanvasPosition(), diffPosition);
		
		//Traverse the canvas and store the rays		
		Vector3d deltaHorizontal = new Vector3d(parallelVersorToCanvas);
		Vector3d deltaVertical = new Vector3d(verticalReferenceVersor);		
		deltaVertical.scale(y*stepSize);
		deltaHorizontal.scale(x*stepSize);		
		Point3d currentCanvasPoint = new Point3d(upperLeftPoint);
		currentCanvasPoint.add(deltaHorizontal);
		currentCanvasPoint.sub(deltaVertical);
		return new Ray( new Point3d(camera.getEyePosition()), new Point3d(currentCanvasPoint));
	}
	
	/*
	 * This method allows to determine the scale factor to reach the canvas vertical or horizontal limit
	 * takes into account if the dimension is even or odd number
	 */	
	private double getScaleFactor( int dimension, double stepSize ){
		if( dimension % 2 == 0 )
			return stepSize/2 * (dimension-1);
		else
			return stepSize * (dimension/2);
	}
	
	/*
	 * For debugging and testing purposes.
	 * 	Build manually an image. This puts 3 vertical bands on the image
	 * 	From left to right: REG, GREEN, BLUE
	 * 	The color bands should overlap, it gives a gradient effect !!
	 * It is just for DEBUGGING and TESTING
	 */
	@SuppressWarnings("unused")
	private Pixel generateTestImagePixel( int x, int y, int width, int height){
		int half_width = width/2;
		 // Start red on left and decrease to zero at center
	    int red = 255 -  (512 * x)/width;
	    if (red < 0 ) 
	    	red = 0;

	    // Green peaks in center
	    int green = 0;
	    if (x < half_width )
	        green =  (512 * x)/width;
	    else
	      green = 255 -  (255 *  (x - half_width))/half_width;

	    // Blue starts from center and peaks at right side.
	    int blue = 0;
	    if (x > half_width)
	        blue =  (255 *  (x - half_width))/half_width;

	    int alpha = 255; // non-transparent
	    return new Pixel(alpha, red, green, blue);		
	}
}

